// Video Output
// class version by regret (ref: bsnes)

#include "burner.h"
#include "effect/effect.h"
#include "vid_support.h"

static InterfaceInfo VidInfo = { NULL, };

wchar_t vidSelect[16] = L"Direct3D";		// Which video plugin is selected
static wchar_t vidActive[16];				// Which video plugin is actived

bool vidOkay = false;

unsigned int vidWidth = 640, vidHeight = 480, vidDepth = 32, vidRefresh = 0;
int vidFullscreen = 0;
int vidFullStretch = 0;							// 1 = stretch to fill the entire window/screen
int vidCorrectAspect = 1;						// 1 = stretch to fill the window/screen while maintaining the correct aspect ratio
int vidVSync = 0;								// 1 = sync blits/pageflips/presents to the screen
int vidTripleBuffer = 0;						// 1 = use triple buffering
int vidRotationAdjust = 1;						// & 1: rotate the graphics for vertical games,  & 2: Reverse flipping for vertical games

#if defined (_WIN32)
HWND hVidWnd = NULL;							// Actual window used for video
#endif

unsigned vidScrnWidth = 0, vidScrnHeight = 0;	// Actual Screen dimensions (0 if in windowed mode)
unsigned vidScrnDepth = 0;						// Actual screen depth

int vidScrnAspectX = 4, vidScrnAspectY = 3;		// Aspect ratio of the display screen
double vidScrnAspect = (double)4 / (double)3;	// Aspect ratio
bool vidAutoScrnAspect = true;					// Automatical Aspect ratio
unsigned int vidAdapter = 0;

static bool vidRecalcPalette;
static unsigned char* vidTransImage = NULL;
static unsigned int* vidTransPalette = NULL;
const int transPaletteSize = 65536;

// VideoInterface
VideoInterface videoInterface;

#if defined (_WIN32)
  #include "vid_ddraw.cpp"
  #include "vid_d3d.cpp"
 #ifndef NO_OPENGL
  #include "vid_wgl.cpp"
 #endif
#endif

const wchar_t* VideoInterface::driver_list()
{
	return

#if defined (_WIN32)
	L"Direct3D;"
 	L"DirectDraw;"
 #ifndef NO_OPENGL
	L"OpenGL;"
 #endif
#endif

  L"None";
}

void VideoInterface::driver(const wchar_t* _driver)
{
	if (p) {
		term();
	}

	if (!_driver || !*_driver) {
		p = new Video();
		return;
	}

#if defined (_WIN32)
	else if (!wcscmp(_driver, L"Direct3D")) {
		p = new VideoD3D();
	}
	else if (!wcscmp(_driver, L"DirectDraw")) {
		p = new VideoDDraw();
	}

	#ifndef NO_OPENGL
	else if (!wcscmp(_driver, L"OpenGL")) {
		p = new VideoWGL();
	}
	#endif
#endif
	else {
		p = new Video();
	}
}

void VideoInterface::term()
{
	if (p) {
		delete p;
		p = 0;
	}
}

int VideoInterface::init()
{
	if (!hScrnWnd) {
		return 1;
	}

	exit();

	if (wcscmp(vidActive, vidSelect)) {
		wcscpy(vidActive, vidSelect);
		driver(vidSelect);
	}

	// show intro
	if (!driverOkay && showEffect) {
		if (!setGameInfo() || p->init()) {
			exit();
			return 1;
		}

		vidOkay = true;
		effect_depth = vidScrnDepth; // set effect depth
		nBurnBpp = 2;

		vidTransPalette = new unsigned int[transPaletteSize];
		vidTransImage = new unsigned char[imageWidth * imageHeight * sizeof(int)];

		setColorFunc();

		if (!vidTransPalette || !vidTransImage) {
			exit();
			return 1;
		}
		return 0;
	}

	// show skin
	HBITMAP hbitmap = NULL;

#if defined (_WIN32)
	BITMAP bitmap;

	if (!driverOkay && vidUsePlaceholder) {
		hbitmap = loadSkin(hScrnWnd);
		GetObject(hbitmap, sizeof(BITMAP), &bitmap);

		imageWidth = bitmap.bmWidth; imageHeight = bitmap.bmHeight;
	}
#endif

	if (driverOkay || hbitmap) {
		if (!setGameInfo() || p->init()) {
			exit();
			return 1;
		}

		nBurnBpp = 4; // Set Burn library Bytes per pixel

		vidOkay = true;

		if (driverOkay && (BurnDrvGetFlags() & BDF_16BIT_ONLY)) {
			nBurnBpp = 2;

			vidTransPalette = new unsigned int[transPaletteSize];
			vidTransImage = new unsigned char[imageWidth * imageHeight * sizeof(int)];

			setColorFunc();

			if (!vidTransPalette || !vidTransImage) {
				exit();
				return 1;
			}
		}
	}

#if defined (_WIN32)
	if (vidOkay && hbitmap) {
		BITMAPINFO bitmapinfo;
		unsigned char* lineBuffer = new unsigned char[bitmap.bmWidth * 4];
		HDC hDC = GetDC(hVidWnd);

		if (hDC && lineBuffer) {
			memset(&bitmapinfo, 0, sizeof(BITMAPINFO));
			bitmapinfo.bmiHeader.biSize = sizeof(BITMAPINFO);
			bitmapinfo.bmiHeader.biWidth = bitmap.bmWidth;
			bitmapinfo.bmiHeader.biHeight = bitmap.bmHeight;
			bitmapinfo.bmiHeader.biPlanes = 1;
			bitmapinfo.bmiHeader.biBitCount = 24;
			bitmapinfo.bmiHeader.biCompression = BI_RGB;

			for (int y = 0; y < imageHeight; y++) {
				unsigned char* pd = vidImage + y * imagePitch;
				unsigned char* ps = lineBuffer;

				GetDIBits(hDC, hbitmap, imageHeight - 1 - y, 1, ps, &bitmapinfo, DIB_RGB_COLORS);

				calcColor(pd, ps, imageWidth, imageHeight);
			}
		}
		if (hDC) {
			ReleaseDC(hVidWnd, hDC);
		}
		delete [] lineBuffer;
	}

	if (hbitmap) {
		DeleteObject(hbitmap);
	}
#endif

	return 0;
}

int VideoInterface::exit()
{
	IntInfoFree(&VidInfo);

	if (!vidOkay) {
		return 1;
	}

	int ret = 0;
	if (p) {
		ret = p->exit();
	}

	freeVidImage();
	filter.exit();

#if defined (_WIN32)
	hVidWnd = NULL;
#endif

	vidOkay = false;

	imageWidth = DEFAULT_IMAGE_WIDTH;
	imageHeight = DEFAULT_IMAGE_HEIGHT;

	nBurnPitch = nBurnBpp = 0;

	delete [] vidTransPalette;
	delete [] vidTransImage;
	vidTransPalette = NULL;
	vidTransImage = NULL;

	return ret;
}

int VideoInterface::doframe(bool redraw)
{
	int ret;

	if (vidTransImage) {
		unsigned short* src = (unsigned short*)vidTransImage;
		unsigned char* dest = vidImage;

		if (vidRecalcPalette) {
			recalcPalette(vidTransPalette);
			vidRecalcPalette = false;
		}

		pBurnDraw = vidTransImage;
		nBurnPitch = imageWidth * 2;

		// ==> Update effect, added by regret
		if (!driverOkay && showEffect) {
			UpdateEffect();
		}
		// <== Update effect

		if (driverOkay) {
			if (redraw) {				// Redraw current frame
				if (BurnDrvRedraw()) {
					BurnDrvFrame();		// No redraw function provided, advance one frame
				}
			} else {
				BurnDrvFrame();			// Run one frame and draw the screen
			}
		}

		ret = p ? p->frame(redraw) : 1;

		// set avi buffer, modified by regret
		aviSetBuffer(pBurnDraw);

		pBurnDraw = NULL;
		nBurnPitch = 0;

		for (int y = 0; y < imageHeight; y++, src += imageWidth, dest += imagePitch) {
			for (int x = 0; x < imageWidth; x++) {
				((unsigned int*)dest)[x] = vidTransPalette[src[x]];
			}
		}

		return ret;
	}

	pBurnDraw = vidImage;
	nBurnPitch = imagePitch;

	if (driverOkay) {
		if (redraw) {					// Redraw current frame
			if (BurnDrvRedraw()) {
				BurnDrvFrame();			// No redraw function provided, advance one frame
			}
		} else {
			BurnDrvFrame();				// Run one frame and draw the screen
		}
	}

	ret = p ? p->frame(redraw) : 1;

	// set avi buffer, modified by regret
	aviSetBuffer(pBurnDraw);

	pBurnDraw = NULL;
	nBurnPitch = 0;

	return ret;
}

int VideoInterface::frame()
{
	if (!vidImage) {
		return 1;
	}

	// ==> Update effect, added by regret
	if (showEffect && vidOkay) {
		return doframe(false);
	}
	// <== Update effect

	if (vidOkay && driverOkay) {
		return doframe(false);
	}
	return 1;
}

int VideoInterface::redraw()
{
	if (vidOkay /* && driverOkay */) {
		return doframe(true);
	}
	return 1;
}

int VideoInterface::recalcPal()
{
	vidRecalcPalette = true;
	return BurnRecalcPal();
}

// If bValidate & 1, the video code should use ValidateRect() to validate the rectangle it draws.
int VideoInterface::paint(int validate)
{
	if (vidOkay) {
		if (!driverOkay) {
			redraw();
		}
		return p ? p->paint(validate) : 1;
	}
	return 1;
}

// reinit video, added by regret
int VideoInterface::reinit()
{
	init();
	if (!vidOkay) {
		return 1;
	}

	if (runPause || !driverOkay) {
		redraw();
	}
	return 0;
}

int VideoInterface::scale(void* rect, unsigned width, unsigned height)
{
	return p ? p->scale(rect, width, height) : 1;
}

void VideoInterface::clear()
{
	if (p) {
		p->clear();
	}
}

void VideoInterface::freeVidImage()
{
	if (fullImage) {
		free(fullImage);
		fullImage = 0;
	}
	vidImage = 0;
}

bool VideoInterface::allocVidImage()
{
	freeVidImage();

	// Allocate an extra line above and below the image to accomodate effects
	imagePitch = imageWidth * 4;
	// for using large skin with D3D+Preview Blitter, modified by regret
	unsigned int memLen = (imageHeight + 4) * imagePitch;
	fullImage = (unsigned char*)malloc(memLen);

	if (fullImage) {
		memset(fullImage, 0, memLen);
		vidImage = fullImage + imagePitch;
		return true;
	}
	return false;
}

bool VideoInterface::setGameInfo()
{
	unsigned gameWidth = imageWidth;
	unsigned gameHeight = imageHeight;
	int gameRotate = 0;

	calcGameAspect(); // calc game aspect

	if (driverOkay) {
		BurnDrvGetVisibleSize(gameWidth, gameHeight);

		if (BurnDrvGetFlags() & BDF_ORIENTATION_VERTICAL) {
			if (vidRotationAdjust & 1) {
				gameRotate |= 1;
			} else {
				swap(gameWidth, gameHeight);
				gameRotate |= (vidRotationAdjust & 2);
			}
		}
		if (BurnDrvGetFlags() & BDF_ORIENTATION_FLIPPED) {
			gameRotate ^= 2;
		}
	}

	if (gameRotate & 1) {
		imageWidth = gameHeight;
		imageHeight = gameWidth;
	} else {
		imageWidth = gameWidth;
		imageHeight = gameHeight;
	}

	nBurnBpp = 4;
	setBurnHighCol();

	bool ret = allocVidImage();

	// init vidsupport
	vidSupport.init(vidImage, gameWidth, gameHeight, gameRotate, imagePitch);

	return ret;
}

void VideoInterface::calcGameAspect()
{
	gameAspect = (double)4 / (double)3;
	if (driverOkay) {
		if ((BurnDrvGetFlags() & BDF_ORIENTATION_VERTICAL) && !(vidRotationAdjust & 1)) {
			BurnDrvGetAspect(gameAspect, false);
		} else {
			BurnDrvGetAspect(gameAspect);
		}
	}
	vidSupport.setAspect(gameAspect);
}

const wchar_t* VideoInterface::getName()
{
	const wchar_t* pszName = NULL;

	if (vidOkay) {
		pszName = vidActive;
	} else {
		pszName = vidSelect;
	}

	if (pszName) {
		return pszName;
	}
	return localiseStringEx(IDS_ERR_UNKNOWN);
}

InterfaceInfo* VideoInterface::get()
{
	if (IntInfoInit(&VidInfo)) {
		IntInfoFree(&VidInfo);
		return NULL;
	}

	if (vidOkay) {
		wchar_t szString[MAX_PATH];
		RECT rect;

		VidInfo.pszModuleName = getName();

		getClientScreenRect(hVidWnd, &rect);
		if (vidFullscreen == 0) {
			_snwprintf(szString, sizearray(szString), L"Running in windowed mode, %ix%i, %ibpp", rect.right - rect.left, rect.bottom - rect.top, vidScrnDepth);
		} else {
			_snwprintf(szString, sizearray(szString), L"Running fullscreen, %ix%i, %ibpp, adapter %d", vidScrnWidth, vidScrnHeight, vidScrnDepth, vidAdapter);
		}

		IntInfoAddStringInterface(&VidInfo, szString);

		_snwprintf(szString, sizearray(szString), L"Source image %ix%i, %ibpp", imageWidth, imageHeight, 32);
		IntInfoAddStringInterface(&VidInfo, szString);

		if (vidTransImage) {
			_snwprintf(szString, sizearray(szString), L"Using generic software 15->%ibpp wrapper", 32);
			IntInfoAddStringInterface(&VidInfo, szString);
		}

		if (vidVSync) {
			_snwprintf(szString, sizearray(szString), L"VSync enabled");
			IntInfoAddStringInterface(&VidInfo, szString);
		}

		if (filter.enable) {
			_snwprintf(szString, sizearray(szString), L"Using pixel filter: %s (%ix zoom)", filter.name(filter.filterId), filter.zoom(filter.filterId));
			IntInfoAddStringInterface(&VidInfo, szString);
		}

	 	if (p) {
			p->get(&VidInfo);
		}
	} else {
		IntInfoAddStringInterface(&VidInfo, L"Video not initialized");
	}

	return &VidInfo;
}

int VideoInterface::set(const wchar_t* name, const void* value)
{
	return p ? p->set(name, value) : 1;
}

int VideoInterface::select(const wchar_t* _driver)
{
	if (!_driver || !*_driver) {
		return 1;
	}

	wcscpy(vidSelect, _driver);
	return 0;
}
